package ru.stepanovdn.trading.server.service.processor

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.stepanovdn.trading.server.entity.Candle
import ru.stepanovdn.trading.server.service.candle.CandleHistoryService
import ru.stepanovdn.trading.server.strategy.AInstrumentByFiatStrategy
import ru.stepanovdn.trading.server.strategy.AStrategy
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.Duration
import java.time.OffsetDateTime
import java.util.*
import java.util.function.Function

/**
 * Сервис для калькуляции решения купить/продать для стратегий с типом instrumentByFiat
 * Торгует при изменении стоимости торгового инструмента относительно его валюты продажи/покупки
 */
@Service
class CalculatorInstrumentByFiatService (
    @Autowired val candleHistoryService: CandleHistoryService
) : ICalculatorService {

    /**
     * Расчет перцентиля по цене инструмента за определенный промежуток
     *
     * @param candle
     * @param percentile
     * @param keyExtractor
     * @param duration
     * @return
     */
    private fun calculate(
        candle: Candle, percentile: Int,
        keyExtractor: Function<in Candle, out BigDecimal>,
        duration: Duration
    ): BigDecimal? {
        return calculate(candle.dateTime!!, candle.figi!!, percentile, keyExtractor, duration)
    }

    /**
     * Расчет перцентиля по цене инструмента за определенный промежуток
     *
     * @param currentDateTime
     * @param figi
     * @param percentile
     * @param keyExtractor
     * @param duration
     * @return
     */
    private fun calculate(
        currentDateTime: OffsetDateTime, figi: String, percentile: Int,
        keyExtractor: Function<in Candle, out BigDecimal>,
        duration: Duration
    ): BigDecimal? {
        // недостаточно данных за промежуток (первая свечка пришла позже начала интервала) - не калькулируем
        val firstCandleDateTime: OffsetDateTime = candleHistoryService.getFirstCandleDateTime(figi)
        if (currentDateTime.minus(duration).isBefore(firstCandleDateTime)) {
            return null
        }

        val startDateTime: OffsetDateTime = currentDateTime.minus(duration)
        val candleHistoryLocal: List<Candle> = candleHistoryService.getCandlesByFigiBetweenDateTimes(
            figi,
            startDateTime, currentDateTime
        )

        // недостаточно данных за промежуток (мало свечек) - не калькулируем
        if (Duration.ofMinutes(30) <= duration && candleHistoryLocal.size < 15
            || Duration.ofHours(1) <= duration && candleHistoryLocal.size < 30
            || Duration.ofHours(2) <= duration && candleHistoryLocal.size < 60
        ) {
            return null
        }

        candleHistoryLocal.sortedWith(Comparator.comparing(keyExtractor))
        val index: Int = candleHistoryLocal.size * percentile / 100
        return Optional.ofNullable(candleHistoryLocal[index]).map(keyExtractor).orElse(null)
    }


    /**
     * Расчет цены для покупки на основе персентиля по цене инструмента за определенный промежуток
     * Будет куплен, если текущая цена < значения персентиля
     *
     * @param strategy
     * @param candle
     * @return
     */
    override fun isShouldBuy(strategy: AInstrumentByFiatStrategy, candle: Candle): Boolean {
        val currentDateTime: OffsetDateTime? = candle.dateTime
        val figi: String? = candle.figi
        val buyCriteria: AInstrumentByFiatStrategy.BuyCriteria? = strategy.buyCriteria
        val valueByPercentile: BigDecimal? = calculate(
            currentDateTime!!, figi!!, buyCriteria!!.lessThenPercentile!!,
            Function<Candle, BigDecimal> {c: Candle -> c.lowestPrice}, strategy.historyDuration
        )

        return valueByPercentile != null && candle.closingPrice!! < valueByPercentile
    }


    /**
     * Расчет цены для продажи на основе персентиля по цене инструмента за определенный промежуток (takeProfitPercentile, stopLossPercentile)
     * Расчет цены для продажи на основе цены покупки (takeProfitPercent, stopLossPercent)
     * Будет куплен если
     * - сработал один из takeProfitPercent, takeProfitPercentile
     * - сработали оба stopLossPercent, stopLossPercentile are happened
     *
     * @param candle
     * @param purchasePrice
     * @return
     */
    override fun isShouldSell(
        strategy: AInstrumentByFiatStrategy,
        candle: Candle,
        purchasePrice: BigDecimal
    ): Boolean {
        val sellCriteria: AInstrumentByFiatStrategy.SellCriteria? = strategy.sellCriteria
        val profitPercent: BigDecimal = candle.closingPrice!!.subtract(purchasePrice)
            .multiply(BigDecimal.valueOf(100))
            .divide(purchasePrice, 4, RoundingMode.HALF_DOWN)
        // profit % > take profit %, profit % > 0.1%
        if (sellCriteria!!.takeProfitPercent != null && profitPercent.toFloat() > sellCriteria.takeProfitPercent!! && profitPercent.toFloat() > 0.1f) {
            return true
        }

        // current price > take profit percentile, profit % > 0.1%
        if (sellCriteria.takeProfitPercentile != null) {
            val valueByPercentile: BigDecimal? = calculate(
                candle, sellCriteria.takeProfitPercentile!!,
                Function<Candle, BigDecimal> {c: Candle -> c.highestPrice}, strategy.historyDuration
            )
            if (valueByPercentile != null && candle.closingPrice!! > valueByPercentile && profitPercent.toFloat() > 0.1f
            ) {
                return true
            }
        }

        var countOfCriteriaToStopLoss = 0
        var countOfSuitableToStopLoss = 0

        // profit % < stop loss %
        if (sellCriteria.stopLossPercent != null) {
            countOfCriteriaToStopLoss++
            if (profitPercent.toFloat() < -1 * sellCriteria.stopLossPercent!!.toFloat()) {
                countOfSuitableToStopLoss++
            }
        }

        // current price < stop loss percentile
        if (sellCriteria.stopLossPercentile != null) {
            countOfCriteriaToStopLoss++
            val valueByPercentile: BigDecimal? = calculate(
                candle, sellCriteria.stopLossPercentile!!,
                Function<Candle, BigDecimal> {c: Candle -> c.lowestPrice}, strategy.historyDuration
            )
            if (valueByPercentile != null && candle.closingPrice!! < valueByPercentile) {
                countOfSuitableToStopLoss++
            }
        }

        if (countOfCriteriaToStopLoss == 0) {
            throw UnsupportedOperationException("No stop loss rule for " + strategy.name)
        }
        return countOfCriteriaToStopLoss == countOfSuitableToStopLoss
    }

    override val strategyType: AStrategy.Type
        get() = AStrategy.Type.instrumentByFiat
}
